Skip to content

库存扣减问题 面试题1

问题描述

java
public class SeckillService {
    private int stock = 10;
    private List<String> userList = new ArrayList<>();

    public void doSeckill(String userId) {
        // 1. 重复购买检查(非线程安全)
        for (int i = 0; i < userList.size(); i++) {
            if (userList.get(i).equals(userId)) {
                System.out.println("用户已购买");
                return;
            }
        }
        // 2. 库存判断与扣减(非原子)
        if (stock > 0) {
            stock--;
        }
        // 3. 记录购买用户(并发修改异常风险)
        userList.add(userId);
        System.out.println("秒杀成功,剩余库存: " + stock);
    }
}

问题列表

  1. 超卖stock > 0stock-- 不是原子操作,多线程下库存扣减会丢失更新,导致最终成功订单数 > 10。
  2. 重复购买userListcontainsadd 非线程安全,同一用户可同时通过检查并重复添加。
  3. 数据一致性问题ArrayList 在并发 add 时可能抛出异常或元素丢失。
  4. 可见性问题:无 volatile 或同步,一个线程修改的 stock 对其它线程不可见。

单机环境解决方案(单 JVM,多线程)

方案一:AtomicInteger.decrementAndGet() + 补偿回滚

java
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class LocalSeckillService_V1 {
    private final AtomicInteger stock = new AtomicInteger(10);
    private final ConcurrentHashMap<String, Boolean> userRecord = new ConcurrentHashMap<>();

    public boolean doSeckill(String userId) {
        // 1. 原子记录用户,若已存在则拒绝
        if (userRecord.putIfAbsent(userId, Boolean.TRUE) != null) {
            System.out.println("用户 " + userId + " 已购买");
            return false;
        }
        // 2. 扣减库存,并判断是否非负
        int remaining = stock.decrementAndGet();
        if (remaining >= 0) {
            System.out.println("秒杀成功,剩余库存: " + remaining);
            return true;
        } else {
            // 库存不足:回滚用户记录 + 加回库存
            userRecord.remove(userId);
            stock.incrementAndGet();
            System.out.println("库存不足,秒杀失败");
            return false;
        }
    }
}

原理putIfAbsent 原子检查并插入,decrementAndGet 原子扣减。若扣减后为负,说明超卖了,立即回滚。

方案二:CAS 自旋扣减(您提供的代码,更高性能)

java
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class LocalSeckillService_V2 {
    private final AtomicInteger stock = new AtomicInteger(10);
    private final ConcurrentHashMap<String, Boolean> userRecord = new ConcurrentHashMap<>();

    public void doSeckill(String userId) {
        // 1. 原子记录用户,若已存在则拒绝
        Boolean previous = userRecord.putIfAbsent(userId, Boolean.TRUE);
        if (previous != null) {
            System.out.println("用户已购买,拒绝重复下单");
            return;
        }
        // 2. 自旋 CAS 扣减库存
        while (true) {
            int current = stock.get();
            if (current <= 0) {
                userRecord.remove(userId);
                System.out.println("库存不足,秒杀失败");
                return;
            }
            if (stock.compareAndSet(current, current - 1)) {
                System.out.println("秒杀成功,剩余库存: " + (current - 1));
                return;
            }
            // CAS 失败则重试(库存值被其他线程修改)
        }
    }
}

原理

  • putIfAbsent 保证同一用户只记录一次。
  • 自旋 CAS 避免使用 decrementAndGet 后需要负数补偿,而是先检查库存 >0,再尝试原子更新,只有更新成功才真正扣减。
  • 如果库存变为 0,后续线程会在 current <= 0 时失败并删除用户记录。
  • 相比方案一,本方案无“先减再补偿”的逻辑,直观且避免了短暂负库存的窗口。

两种方案对比

  • 方案一代码更短,每次仅一次 CAS(decrementAndGet),但可能出现短暂的负数库存(立即补偿不影响最终结果)。
  • 方案二 CAS 自旋可能在高竞争时多次重试,但逻辑更清晰,且不会出现负数。实际性能差异微小,均可用于单机秒杀。

集群环境解决方案(多 JVM,分布式)

核心方案:Redis Lua 脚本(原子、高性能)

java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class RedisSeckillService {
    private final JedisPool jedisPool;
    private final String productId;

    private static final String LUA_SCRIPT =
        "local userId = ARGV[1]\n" +
        "local userSetKey = KEYS[1]\n" +
        "local stockKey = KEYS[2]\n" +
        "if redis.call('SISMEMBER', userSetKey, userId) == 1 then return 0 end\n" +
        "local remaining = redis.call('DECR', stockKey)\n" +
        "if remaining >= 0 then\n" +
        "    redis.call('SADD', userSetKey, userId)\n" +
        "    return remaining\n" +
        "else\n" +
        "    redis.call('INCR', stockKey)\n" +
        "    return -1\n" +
        "end";

    private final String scriptSha;

    public RedisSeckillService(JedisPool jedisPool, String productId) {
        this.jedisPool = jedisPool;
        this.productId = productId;
        try (Jedis jedis = jedisPool.getResource()) {
            this.scriptSha = jedis.scriptLoad(LUA_SCRIPT);
        }
    }

    public int doSeckill(String userId) {
        String userSetKey = "seckill:users:" + productId;
        String stockKey   = "seckill:stock:" + productId;
        try (Jedis jedis = jedisPool.getResource()) {
            Object res = jedis.evalsha(scriptSha, 2, userSetKey, stockKey, userId);
            return ((Long) res).intValue();  // 1成功,0已购,-1库存不足
        }
    }

    public void initStock(int totalStock) {
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.set("seckill:stock:" + productId, String.valueOf(totalStock));
            jedis.del("seckill:users:" + productId);
        }
    }
}

后备方案:分布式锁 + 数据库(低性能,强一致性)

java
@Service
public class DbSeckillService {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public boolean doSeckill(Long productId, String userId) {
        RLock lock = redissonClient.getLock("seckill:product:" + productId);
        try {
            if (lock.tryLock(0, 3, TimeUnit.SECONDS)) {
                // 唯一约束保证不重复购买
                int count = jdbcTemplate.queryForObject(
                    "SELECT COUNT(*) FROM purchase WHERE product_id=? AND user_id=?",
                    Integer.class, productId, userId);
                if (count > 0) return false;

                int updated = jdbcTemplate.update(
                    "UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0",
                    productId);
                if (updated == 0) return false;

                jdbcTemplate.update(
                    "INSERT INTO purchase(product_id, user_id, create_time) VALUES(?,?,NOW())",
                    productId, userId);
                return true;
            }
            return false;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) lock.unlock();
        }
    }
}

方案对比与选型建议

环境推荐方案性能复杂度说明
单机CAS 自旋(方案二)或 decrementAndGet + 补偿极高直接使用 AtomicInteger + ConcurrentHashMap
集群(高并发)Redis Lua 脚本极高无锁,原子性由 Redis 保证,最适合秒杀
集群(低并发/必须用DB)分布式锁 + 数据库避免引入 Redis 时可选,但 QPS 有限

最终建议

  • 单机:优先采用您提供的 CAS 自旋方案,代码直观且不会出现负数库存。
  • 集群:必须使用 Redis Lua 脚本,彻底杜绝超卖和重复下单。